【第1761期】一个前端的 functor,applicative functor,monad 初探
前言
今日早读文章由酷家乐@Gloria投稿分享。
正文从这开始~~
在使用 ramda 的时候,经常会在文档中出现一些概念性的名词,比如 functor,applicative functor,monad。这些“高端词汇”都是啥意思,宝宝很好奇,索性就来探一探。
其实解释这些概念,更接近本质方式是数学形式的推导,但是由于水平和精力有限,这篇文章会结合 haskell 中的定义,探索它们在 javascript 和 ramda 库中的体现。
Functor
先来看一看在 haskell 中是如何定义一个函子的,任何可以被 fmap (a -> b)映射的类型实例,该类型就是函子。(其实看不懂也无所谓~)
class Functor f where
fmap :: (a -> b) -> f a -> f b
数组
先来个简单的例子,在 ramda 中如何实现 Array.prototype.map 的功能。
可以使用 R.map ,像下面这样
const a = [1, 2, 3];
const b = R.map(String, a); // ['1', '2', '3']
换一个角度想这个过程:a 是一个容器,里面有3个值,通过函数 R.map(String) 映射出了另外一个同样有3个值的容器 b。
a容器和b容器其实就是 functor,简单地说就是可被映射的容器就是 functor,函子是可包含值的容器。
函数
其实函数也是 functor,可将函数视为包裹着值的容器。
恒值函数
先看一个比较简单的函数,() => 1,这个是一个包裹着值 1 的容器,我们可以将这个容器映射为一个包裹着 2 的容器:
const a = () => 1;
const b = R.map(x => x+1, a); // () => 2
恒值函数是一个包裹着恒定值的函数,利用 ramda 我们可以像下面一样创建这样一个容器:
R.always(2); // () => 2;
在日常开发中,我们接触到的大多数函数都不是恒值函数,而是普通函数,接下来说一说普通函数。
普通函数
普通函数也是函子,可以想象 x => x + 1, 我们可以把这个函数视为一个容器,这个容器的值就是 参数 + 1 ,一个不确定的值而已。
既然我们可以把函数视为一个 包含不确定值的容器。
思考一个问题:如何将 x => x + 1 这样一个容器,映射成 x => (x + 1) * 2 ?
你可能会写出下面这样的代码:
const a = x => x + 1;
const b = x => a(x) * 2;
b(1); // 4
换成 ramda 的写法 :
const a = x => x + 1;
const b = R.map(x => x * 2, a);
b(1); // 4
容器 a 映射成了 b ,映射过程:生成了一个 b 容器,它的值是 a中不确定的值*2。
接下来我们来看一看 functor 的升级版 applicative functor。
Applicative Functor
先看一看 haskell 中的定义:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
首先 Applicative Functor 必须是 Functor ,另外存在 pure 方法接收一个值 a ,返回一个包裹了 a 的容器。
其次,支持 <*>
函数,在 Haskell 中可以像下面这样操作 applicative functor :
pure (+10) <*> Just 9 --> Just 19
--> 或者使用 applicative 风格
(+10) <$> <*> Just 9 --> Just 19
R.ap
在 ramda 中数组和函数同样也都是 applicative functor,我们可以利用 R.ap 函数充当 Haskell 中 <*>
的角色。
先来看一看 R.ap 函数的通用定义,是不是和 applicative functor 的 <*>
函数一样:
Apply f => f (a → b) → f a → f b
函数
有一个包裹着映射规则 x => x + 1 的容器 a 和包裹着值 2 的容器 b。
思考:如何将 a 中的映射规则应用到容器 b 的值上,并将产生的新值放到一个新的容器里。
我们可以利用 R.ap 函数:
const a = R.always(x => x + 1); // 容器 a
const b = R.always(2); // 容器 b
const c = R.ap(a, b); // 容器 c:() => 3
数组
数组也是类似,在数组中包裹一个函数 x => x + 1 ,然后在另一个数组上应用这个数组。
const a = [1];
const b = [x => x + 1];
R.ap(b, a); // [2]
如果数组 a 中有 a 个元素,数组 b 中有 b 个元素,那么最终会生成 a*b 个元素的数组
const a = [1, 2];
const b = [x => x + 1, x => x * 2];
R.ap(b, a); // [2, 2, 3, 4]
见识到 applicative functor 之后,接下来感受一下 Monad 是什么。
Monad
Haskell 中 Monad 定义:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
简单来说,首先 return 函数接收一个值,返回一个包裹这个值的容器。其次对于 >>= 函数来说,接收一个容器和对其中值的映射规则,返回一个新的容器,这个容器就是映射规则返回的容器。
换一种角度,其实 Monad 就是在说某种特殊的函数满足结合率这件事,>>= 函数描述了 m 这种容器在 >>= 下满足结合率的这一特性,然后 return 在其中扮演单位元的角色,任何容器通过 >>= 函数与 return 组合,返回的一定是自身。
在 javascript 中,最典型的 monad 就是 Promise。
Promise
先创建一个 Promise:
const getUser = new Promise(...);
我们创建了一个 Promise 实例。那么如何去消费这个实例产出的值呢?大家都知道,可以用 then 方法:
const consumer = user => {...};
getUser.then(consumer);
在 consumer 函数中去消费 user。如果我们想在拿到 user 之后再去进行异步操作怎么办?改写上面的例子
const getDetailByUser = user => new Promise(...);
getUser.then(getDetailByUser).then(...);
我们再来简化地描述这个过程:
getUser :: Promise<user>
getDetailByUser :: user -> Promise<detail>
把 Promise 看做一个包裹着未来值的容器 m,把 user 用 a 代替,detail 用 b 代替,省去 <>
:
getUser :: m a
getDetailByUser :: a -> m b
其中 then 方法是不是就有些类似 >>= 函数。
then :: m a ->(a -> m b)-> m b
在 then 函数的作用下类似 a -> m b 这样的过程满足结合率,那么 return 的单位元的职责是由谁来承担的呢?
getUser.then(Promise.resolve).then(getDetailByUser)
getUser.then(getDetailByUser).then(Promise.resolve)
这两行代码的效果都是一样的,Promise.resolve 承担着单位元的作用,任何过程 a->Promise<b>
与 Promise.resolve 通过 then 组合(左结合或右结合),其结果都是 a->Promise<b>
。
通过对比在 Haskell 中的定义, Promise 符合 Monad 的形式。
通常 monad 模式会用来封装一些副作用,使得这部分"不纯"的逻辑与外部“纯洁”的逻辑隔离开,比如在 haskell 中的 IO 类型。
这篇文章算是给前段时间的困惑画上了一个句号吧,如果有些地方理解有误,还请多多指出。
参考资料:
《 Haskell 趣学指南 》
IO and monads
写给小白的Monad指北
关于本文 作者:@Gloria 原文:https://zhuanlan.zhihu.com/p/88741757
他曾分享过